package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.product.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {

   @Autowired
   private StringRedisTemplate stringRedisTemplate;

   /**
    * synchronized或Lock 叫做本地锁。只能只对于当前JVM虚拟机的多个线程加锁进行并发控制。
    * 在集群环境下，多个JVM虚拟机，本地锁是失效的。必须通过分布式锁来解决并发问题：
    *    分布式锁解决方案：
             1. 基于数据库实现分布式锁
             2. 基于缓存（ Redis等）
             3. 基于Zookeeper
      问题：
         1.死锁
               解决：利用锁过期时间自动释放锁，避免死锁。
         2.删除锁问题。避免删除其他人的锁。
               解决：锁的值设置自己的值。释放锁时，判断锁的值是否是自己的值，是才释放锁。否则，不用理会。
               还有问题：缺乏原子性。
                if(uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){  //避免删除别人的锁。
                     stringRedisTemplate.delete("lock");
                }
               如何保证，判断和删除是原子性的呢？
                  解决：Lua脚本

    */
   /*@Override
   public void testLock() {
      //设置锁和设置过期时间不能分开用两行代码来设置，因为redis执行两个命令，不是原子的。
      //Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
      //stringRedisTemplate.expire("lock",60, TimeUnit.SECONDS);
      //设置锁和设置过期时间用一行代码，能保证原子性，因为redis执行一条命令。
      String uuid = UUID.randomUUID().toString().replaceAll("-","");
      Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);
      if(ifAbsent){ //加分布式锁成功
         //int i = 1/0; // 异常或服务器实例宕机，导致锁无法释放。导致死锁。
         // 查询Redis中的num值
         String value = (String)this.stringRedisTemplate.opsForValue().get("num");
         // 没有该值return
         if (StringUtils.isBlank(value)){
            return ;
         }
         // 有值就转成成int
         int num = Integer.parseInt(value);
         // 把Redis中的num值+1
         this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
         try {
            Thread.sleep(60000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         //释放分布式锁
        *//* if(uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){  //避免删除别人的锁。但判断和删除操作并不是原子的 （依然可能删除别人的锁）。
            stringRedisTemplate.delete("lock");
         }*//*
         // lua脚本保证原子性
         String script = "if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
                 "\treturn redis.call('del',KEYS[1])\n" +
                 "else\n" +
                 "\treturn 0\n" +
                 "end";
         DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script,Boolean.class);
         Boolean result = stringRedisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);

      }else{//加布式锁失败
         try {
            Thread.sleep(1000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         testLock();
      }
   }*/




   @Override
   public void testLock() {

      String uuid = UUID.randomUUID().toString().replaceAll("-","");
      Boolean ifAbsent = this.tryLock("lock",uuid,30L); //加锁过期时间3秒
      if(ifAbsent){ //加分布式锁成功
         //int i = 1/0; // 异常或服务器实例宕机，导致锁无法释放。导致死锁。
         // 查询Redis中的num值
         String value = (String)this.stringRedisTemplate.opsForValue().get("num");
         // 没有该值return
         if (StringUtils.isBlank(value)){
            return ;
         }
         // 有值就转成成int
         int num = Integer.parseInt(value);
         // 把Redis中的num值+1
         this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
         try {
            Thread.sleep(25000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }

         //测试可重入锁
         //testSubLock(uuid);

         //解锁
         this.unlock("lock",uuid);
      }else{//加布式锁失败
         try {
            Thread.sleep(1000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         testLock();
      }
   }


   // 测试可重入性
   private void testSubLock(String uuid){
      // 加锁
      Boolean lock = this.tryLock("lock", uuid, 3L);

      if (lock) {
         System.out.println("分布式可重入锁。。。");

         this.unlock("lock", uuid);
      }
   }

   //加锁
   private Boolean tryLock(String lockName, String uuid, Long expire){
      String script = "if(redis.call('exists' , KEYS[1]) == 0 ) then" +
              " redis.call('hset' , KEYS[1] ,ARGV[1] , 1)" +
              " redis.call('expire' , KEYS[1] , ARGV[2])" +
              "  return 1" +
              " elseif(redis.call('hexists' , KEYS[1],ARGV[1])==1) then" +
              " redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1)" +
              "  return 1" +
              " else" +
              "  return 0" +
              " end";
      if (!this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
         try {
            // 没有获取到锁，重试
            Thread.sleep(200);
            tryLock(lockName, uuid, expire);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }


      // 锁续期
      this.renewTime(lockName, uuid, expire );


      // 获取到锁，返回true
      return true;
   }

   //解锁
   private void unlock(String lockName, String uuid){
      String script = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +
              "    return nil;" +
              "end;" +
              "if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +
              "    return 0;" +
              "else" +
              "    redis.call('del', KEYS[1]);" +
              "    return 1;" +
              "end;";
      // 这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中，三个返回值含义如下：
      // 1 代表解锁成功，锁被释放
      // 0 代表可重入次数被减 1
      // null 代表其他线程尝试解锁，解锁失败
      Long result = this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),  Arrays.asList(lockName), uuid);
      // 如果未返回值，代表尝试解其他线程的锁
      if (result == null) {
         throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName: "
                 + lockName + " with request: "  + uuid);
      }
   }




   /**
    * 锁延期
    * 线程等待超时时间的2/3时间后,执行锁延时代码,直到业务逻辑执行完毕,因此在此过程中,其他线程无法获取到锁,保证了线程安全性
    * @param lockName
    * @param expire 单位：毫秒
    */
   private void renewTime(String lockName, String uuid, Long expire){
      String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
      new Thread(() -> {
         while (this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
            System.out.println("锁续期...renewTime : "+ new Date());
            try {
               // 到达过期时间的2/3时间，自动续期
               Thread.sleep(expire* 1000 / 3);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }).start();
   }



}